﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using System.Diagnostics;
using Centny.PdfReader.Types;
using Centny.PdfReader.Pdf;
using Centny.PdfReader.Tools;
namespace Centny.PdfReader.IO
{
    public class PdfLexer : ByteReader, PdfIDocumentVisible
    {
        public PdfLexer(Stream r)
            : base(r)
        {
        }

        public enum ObjectType
        {
            Name = 0, String, Array, Double, Dictionary, Reference, Long, None
        }
        //
        public PdfDocument Doc { get; set; }
        public const String TOKEN_BEG = "[</(";
        public const String TOKEN_BEG2 = "[/(";
        public const String TOKEN_END = "]>/)";
        public const String TOKEN_SEQ = TOKEN_BEG + TOKEN_END;
        public PdfDictionary NextDictionary()
        {
            char t;
            PdfDictionary dict = new PdfDictionary();
            while (!this.Eof)
            {
                this.SkipWhite();
                t = this.PeekChar();
                if ('>' == t)
                {
                    this.ReadChar();
                    this.ReadChar();
                    break;
                }
                PdfName key = this.NextName();
                PdfClass val = null;
                val = this.Next(AryValType(key.Name));
                Asserts.Assert(val != null);
                dict[key] = val;
            }
            return dict;
        }
        //public PdfInt NextInt()
        //{
        //    return new PdfInt(this.NextIntToken());
        //}
        public PdfDouble NextDouble()
        {
            return new PdfDouble(this.NextDoubleToken());
        }
        public PdfLong NextLong()
        {
            return new PdfLong(this.NextLongToken());
        }
        public PdfString NextString(bool hex)
        {
            using (MemoryStream ms = new MemoryStream())
            {
                char ch;
                long begl = this.Position;
                while (!this.Eof)
                {
                SkipChar:
                    ch = (char)this.ReadByte();
                    switch (ch)
                    {
                        case '>':
                            return new PdfString(ms.ToArray(), true);
                        case ')':
                            return new PdfString(ms.ToArray(), false);
                        case '\\':
                            ch = (char)this.ReadByte();
                            switch (ch)
                            {
                                case 'n':
                                    ch = '\n';
                                    break;

                                case 'r':
                                    ch = '\r';
                                    break;

                                case 't':
                                    ch = '\t';
                                    break;

                                case 'b':
                                    ch = '\b';
                                    break;

                                case 'f':
                                    ch = '\f';
                                    break;

                                case '(':
                                    ch = '(';
                                    break;

                                case ')':
                                    ch = ')';
                                    break;

                                case '\\':
                                    ch = '\\';
                                    break;

                                case '\n':
                                    goto SkipChar;

                                default:
                                    if (Char.IsDigit(ch))
                                    {
                                        // Octal character code
                                        Debug.Assert(ch < '8', "Illegal octal digit.");
                                        int n = ch - '0';
                                        if (Char.IsDigit((char)this.PeekChar()))
                                        {
                                            n = n * 8 + this.ReadByte() - '0';
                                            if (Char.IsDigit((char)this.PeekChar()))
                                            {
                                                n = n * 8 + this.ReadByte() - '0';
                                            }
                                        }
                                        ch = (char)n;
                                    }
                                    else
                                    {
                                        //TODO
                                        Debug.Assert(false, "Not implemented; unknown escape character.");
                                    }
                                    break;
                            }
                            break;
                    }
                    ms.WriteByte((byte)ch);
                }
            }
            throw new InvalidDataException("Invalid String format");
        }
        public PdfString NextLineString()
        {
            return new PdfString(this.ReadLine());
        }
        public PdfName NextName()
        {
            this.ReadChar();
            return new PdfName("/" + this.NextToken(PdfLexer.TOKEN_SEQ));
        }
        public PdfClass Next(ObjectType type = ObjectType.None)
        {
            this.SkipWhite();
            char t = (char)this.PeekChar();
            if (Char.IsNumber(t) || t == '-')
            {
                String token = this.NextToken(PdfLexer.TOKEN_SEQ);
                if (token.IndexOf('.') > -1)
                {
                    return new PdfDouble(double.Parse(token));
                }
                this.PushState();
                PdfLong n1 = this.Next() as PdfLong;
                if (n1 == null)
                {
                    this.PopState();
                    return new PdfLong(long.Parse(token));
                }
                String r = this.NextToken(PdfLexer.TOKEN_SEQ);
                if (!"R".Equals(r))
                {
                    this.PopState();
                    return new PdfLong(long.Parse(token));
                }
                PdfReference pref = new PdfReference();
                pref.Num = int.Parse(token);
                pref.ModifyTimes = n1.AsLong;
                pref.Type = r;
                return pref;
            }
            else if (t == 't' || t == 'f')
            {
                String v = this.NextToken();
                if (v.Equals("true"))
                {
                    return new PdfBoolean(true);
                }
                else if (v.Equals("false"))
                {
                    return new PdfBoolean(false);
                }
                else
                {
                    return new PdfOperator(v);
                }
            }
            else if (PdfLexer.TOKEN_BEG.IndexOf(t) > -1)
            {
                return this.Next(t, type);
            }
            else
            {
                String v = this.NextToken(TOKEN_BEG);
                if (String.IsNullOrEmpty(v))
                {
                    return null;
                }
                else
                {
                    return new PdfOperator(v);
                }
            }
        }
        public byte[] NextByte(int length)
        {
            byte[] data = new byte[length];
            int len = this.Read(data, 0, length);
            if (len != length)
            {
                throw new InvalidDataException("Not enough data");
            }
            else
            {
                return data;
            }
        }
        public PdfReference NextReference()
        {
            PdfReference prf = new PdfReference();
            prf.Num = this.NextIntToken();
            prf.ModifyTimes = this.NextIntToken();
            prf.Type = this.NextToken();
            return prf;
        }
        public PdfArray NextArray(ObjectType type = ObjectType.None)
        {
            PdfArray ary = new PdfArray();
            char t;
            while (!this.Eof)
            {
                this.SkipWhite();
                t = (char)this.PeekChar();
                if (']' == t)
                {
                    this.ReadChar();
                    break;
                }
                PdfClass nobj = null;
                switch (type)
                {
                    case ObjectType.Long:
                        nobj = this.NextLong();
                        break;
                    case ObjectType.Double:
                        nobj = this.NextLong();
                        break;
                    case ObjectType.Reference:
                        nobj = this.NextReference();
                        break;
                    case ObjectType.Name:
                        nobj = this.NextName();
                        break;
                    default:
                        nobj = this.Next();
                        break;
                }
                ary.Objects.Add(nobj);
            }
            return ary;
        }
        private PdfClass Next(char p, ObjectType type = ObjectType.None)
        {
            switch (p)
            {
                case '<':
                    this.ReadChar();
                    char t = (char)this.PeekChar();
                    if ('<' == t)
                    {
                        this.ReadChar();
                        return this.NextDictionary();
                    }
                    else
                    {
                        return this.NextString(true);
                    }
                case '/':
                    return this.NextName();
                case '[':
                    this.ReadChar();
                    return this.NextArray(type);
                case '(':
                    this.ReadChar();
                    return this.NextString(false);

            }
            throw new InvalidDataException("Invalid Data format");
        }
        public void SkipWhite()
        {
            char t;
            while (!this.Eof)
            {
                t = (char)this.PeekChar();
                if (Char.IsWhiteSpace(t))
                {
                    this.ReadChar();
                }
                else
                {
                    break;
                }
            }
        }
        public String NextToken(String ext = "")
        {
            this.SkipWhite();
            StringBuilder builder = new StringBuilder();
            char t;
            while (!this.Eof)
            {

                t = this.PeekChar();
                if (ext.IndexOf(t) > -1 || Char.IsWhiteSpace(t))
                {
                    break;
                }
                builder.Append(this.ReadChar());
            }
            return builder.ToString();
        }
        public int NextIntToken()
        {
            return int.Parse(this.NextToken());
        }
        public double NextDoubleToken()
        {
            return double.Parse(this.NextToken());
        }
        public long NextLongToken()
        {
            return long.Parse(this.NextToken());
        }
        public PdfPointer NextPointer()
        {
            PdfPointer p = new PdfPointer();
            p.Offset = this.NextLongToken();
            p.MTime = this.NextIntToken();
            p.State = this.NextToken()[0];
            return p;
        }
        public PdfObject NextObject(ObjectType type = ObjectType.None)
        {
            int num = this.NextIntToken();
            int mtime = this.NextIntToken();
            String obj = this.NextToken(PdfLexer.TOKEN_SEQ);
            PdfObject tobj;
            PdfClass val = this.Next(type);
            PdfDictionary dict = val as PdfDictionary;
            String token = this.NextToken();
            if (dict != null && token.Equals("stream"))
            {
                PdfStream ps = new PdfStream();
                this.SkipWhite();
                ps.Position = this.Position;
                PdfStreamObject so = new PdfStreamObject();
                so.InnerObj = dict;
                so.Stream = ps;
                tobj = so;
                tobj.Num = num;
                tobj.ModifyTimes = mtime;
                tobj.Obj = obj;
                tobj.Doc = this.Doc;
                long op = this.Position;
                long len = so.Length;
                this.Position = op + len;
                token = this.NextToken();
                Asserts.Assert(token.Equals("endstream"));
                token = this.NextToken();
            }
            else
            {
                PdfClassObject co = new PdfClassObject();
                co.InnerObj = val;
                tobj = co;
                tobj.Num = num;
                tobj.ModifyTimes = mtime;
                tobj.Obj = obj;
                tobj.Doc = this.Doc;
            }
            Asserts.Assert(token.Equals("endobj"));
            return tobj;
        }

        public static ObjectType AryValType(String key)
        {
            if (FontKeys.Widths.Equals(key))
            {
                return ObjectType.Long;
            }
            else
            {
                return ObjectType.None;
            }
        }
        //public String ReadLine()
        //{
        //    StringBuilder sb = new StringBuilder();
        //    char c;
        //    while ((c = this.ReadChar()) != '\n')
        //    {
        //        sb.Append(c);
        //    }
        //    return sb.ToString();
        //}
        //public String ReadToEnd()
        //{
        //    StringBuilder sb = new StringBuilder();
        //    while (!this.Eof)
        //    {
        //        char c = this.ReadChar();
        //        sb.Append(c);
        //    }
        //    return sb.ToString();
        //}
        public static long LastTime { get; set; }
        public static long Timestamp()
        {
            long ticks = DateTime.UtcNow.Ticks - DateTime.Parse("01/01/1970 00:00:00").Ticks;
            ticks /= 10000;
            return ticks;
        }
        public static void Beg()
        {
            LastTime = Timestamp();
        }
        public static long Delay()
        {
            long now = Timestamp();
            long d = now - LastTime;
            LastTime = now;
            return d;
        }
        public static void End(String msg = "time")
        {
            System.Diagnostics.Debug.WriteLine(msg + ":" + Delay());
        }
    }
}
